/*
 * Copyright 2004-2005 The Apache Software Foundation or its licensors,
 *                     as applicable.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.session;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;

import javax.jcr.NamespaceException;
import javax.jcr.NamespaceRegistry;
import javax.jcr.RepositoryException;
import javax.jcr.UnsupportedRepositoryOperationException;

import org.apache.xerces.util.XMLChar;

public final class SessionNamespaceRegistry implements NamespaceRegistry {

    /** The underlying global namespace registry. */
    private final NamespaceRegistry registry;

    /** Local prefix to namespace URI mappings. */
    private final Properties prefixToURI;

    /** Local namespace URI to prefix mappings. */
    private final Properties uriToPrefix;

    /** The global prefixes hidden by local mappings. */
    private final Set hiddenPrefixes;

    /**
     * Creates a local namespace registry based on the given global
     * namespace registry. The local namespace mappings are initially empty.
     *
     * @param registry global namespace registry
     */
    public SessionNamespaceRegistry(NamespaceRegistry registry) {
        this.registry = registry;
        this.prefixToURI = new Properties();
        this.uriToPrefix = new Properties();
        this.hiddenPrefixes = new HashSet();
    }

    /**
     * Creates a local namespace mapping. See the JCR specification
     * for the details of this rather complex operation.
     * <p>
     * This method implements the specified semantics of the
     * Session.setNamespacePrefix method. Session implementations can use
     * this method as follows:
     * <pre>
     *     NamespaceRegistry registry = new SessionNamespaceRegistry(
     *             getWorkspace().getNamespaceRegistry());
     *
     *     public void setNamespacePrefix(String prefix, String uri)
     *             throws NamespaceException, RepositoryException {
     *         return registry.registerNamespace(prefix, uri);
     *     }
     * </pre>
     *
     * @param prefix namespace prefix
     * @param uri    namespace URI
     * @throws NamespaceException  if the given namespace mapping is invalid
     * @throws RepositoryException on repository errors
     * @see NamespaceRegistry#registerNamespace(String, String)
     * @see javax.jcr.Session#setNamespacePrefix(String, String)
     */
    public void registerNamespace(String prefix, String uri)
            throws NamespaceException, RepositoryException {
        if (prefix.length() == 0) {
            throw new NamespaceException("The empty prefix is reserved");
        } else if (uri.length() == 0) {
            throw new NamespaceException("The empty namespace URI is reserved");
        } else if (prefix.toLowerCase().startsWith("xml")) {
            throw new NamespaceException("The xml* prefixes are reserved");
        } else if (!XMLChar.isValidNCName(prefix)) {
            throw new NamespaceException("Invalid prefix format");
        }

        // Note: throws a NamespaceException if the URI is not registered
        String oldPrefix = getPrefix(uri);

        String oldURI;
        try {
            oldURI = getURI(prefix);
        } catch (NamespaceException e) {
            oldURI = null;
        }
        if (oldURI == null) {
            hiddenPrefixes.add(oldPrefix);
            prefixToURI.remove(oldPrefix);
            prefixToURI.setProperty(prefix, uri);
            uriToPrefix.setProperty(uri, prefix);
        } else if (!uri.equals(oldURI)) {
            throw new NamespaceException(
                    "Cannot hide an existing namespace mapping");
        }
    }

    /**
     * Not implemented. It is not possible to unregister namespaces from
     * a session, you need to access the global namespace registry directly.
     *
     * @param prefix namespace prefix
     * @throws UnsupportedRepositoryOperationException always thrown
     */
    public void unregisterNamespace(String prefix)
            throws UnsupportedRepositoryOperationException {
        throw new UnsupportedRepositoryOperationException();
    }

    /**
     * Returns the currently mapped namespace prefixes. The returned set
     * contains all locally mapped prefixes and those global prefixes that
     * have not been hidden by local mappings.
     * <p>
     * This method implements the specified semantics of the
     * Session.getNamespacePrefixes method. Session implementations can use
     * this method as follows:
     * <pre>
     *     NamespaceRegistry registry = new SessionNamespaceRegistry(
     *             getWorkspace().getNamespaceRegistry());
     *
     *     public String getNamespacePrefixes() throws RepositoryException {
     *         return registry.getPrefixes();
     *     }
     * </pre>
     *
     * @return namespace prefixes
     * @throws RepositoryException on repository errors
     * @see NamespaceRegistry#getPrefixes()
     * @see javax.jcr.Session#getNamespacePrefixes()
     */
    public String[] getPrefixes() throws RepositoryException {
        HashSet prefixes = new HashSet();
        prefixes.addAll(Arrays.asList(registry.getPrefixes()));
        prefixes.removeAll(hiddenPrefixes);
        prefixes.addAll(prefixToURI.keySet());
        return (String[]) prefixes.toArray(new String[prefixes.size()]);
    }

    /**
     * Returns the registered namespace URIs. This method call is simply
     * forwarded to the underlying global namespace registry as it is not
     * possible to locally add new namespace URIs.
     *
     * @return namespace URIs
     * @throws RepositoryException on repository errors
     * @see NamespaceRegistry#getURIs()
     */
    public String[] getURIs() throws RepositoryException {
        return registry.getURIs();
    }

    /**
     * Returns the namespace URI that is mapped to the given prefix.
     * Returns the local namespace mapping if the prefix is locally
     * mapped, otherwise falls back to the underlying global namespace
     * registry unless the prefix has been hidden by local namespace
     * mappings.
     * <p>
     * This method implements the specified semantics of the
     * Session.getNamespaceURI method. Session implementations can use
     * this method as follows:
     * <pre>
     *     NamespaceRegistry registry = new SessionNamespaceRegistry(
     *             getWorkspace().getNamespaceRegistry());
     *
     *     public String getNamespaceURI(String prefix)
     *             throws NamespaceException, RepositoryException {
     *         return registry.getURI(prefix);
     *     }
     * </pre>
     *
     * @param prefix namespace prefix
     * @return namespace URI
     * @throws NamespaceException  if the prefix is not registered or
     *                             currently visible
     * @throws RepositoryException on repository errors
     * @see NamespaceRegistry#getURI(String)
     * @see javax.jcr.Session#getNamespaceURI(String)
     */
    public String getURI(String prefix)
            throws NamespaceException, RepositoryException {
        String uri = prefixToURI.getProperty(prefix);
        if (uri != null) {
            return uri;
        } else if (!hiddenPrefixes.contains(prefix)) {
            return registry.getURI(prefix);
        } else {
            throw new NamespaceException(
                    "Namespace mapping not found for prefix " + prefix);
        }
    }

    /**
     * Returns the prefix that is mapped to the given namespace URI.
     * Returns the local prefix if the namespace URI is locally mapped,
     * otherwise falls back to the underlying global namespace registry.
     * <p>
     * This method implements the specified semantics of the
     * Session.getNamespacePrefix method. Session implementations can use
     * this method as follows:
     * <pre>
     *     NamespaceRegistry registry = new SessionNamespaceRegistry(
     *             getWorkspace().getNamespaceRegistry());
     *
     *     public String getNamespacePrefix(String uri)
     *             throws NamespaceException, RepositoryException {
     *         return registry.getPrefix(uri);
     *     }
     * </pre>
     *
     * @param uri namespace URI
     * @return namespace prefix
     * @throws NamespaceException  if the namespace URI is not registered
     * @throws RepositoryException on repository errors
     * @see NamespaceRegistry#getPrefix(String)
     * @see javax.jcr.Session#getNamespacePrefix(String)
     */
    public String getPrefix(String uri)
            throws NamespaceException, RepositoryException {
        String prefix = uriToPrefix.getProperty(uri);
        if (prefix != null) {
            return prefix;
        } else {
            return registry.getPrefix(uri);
        }
    }

}
